Skip to content

Commit dd23ffb

Browse files
SLourencoanbernar
andauthored
Create the id generator capability for Denmark (#45)
* Create the id generator capability for Denmark This exposes the generateId method, which will generate a fake identifier based on citizen data Co-authored-by: André Bernardino <[email protected]>
1 parent 2671f47 commit dd23ffb

File tree

5 files changed

+203
-0
lines changed

5 files changed

+203
-0
lines changed

src/main/java/com/github/reducktion/socrates/Socrates.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import com.github.reducktion.socrates.extractor.Citizen;
66
import com.github.reducktion.socrates.extractor.CitizenExtractor;
7+
import com.github.reducktion.socrates.generator.IdGenerator;
78
import com.github.reducktion.socrates.validator.IdValidator;
89

910
/**
@@ -37,4 +38,17 @@ public Optional<Citizen> extractCitizenFromId(final String id, final Country cou
3738
final CitizenExtractor citizenExtractor = CitizenExtractor.newInstance(country);
3839
return citizenExtractor.extractFromId(id, idValidator);
3940
}
41+
42+
/**
43+
* Generates an National Identification Number based on a citizen information.
44+
*
45+
* @param citizen the citizen information
46+
* @param country the country of the national identification number
47+
* @return national identifier string
48+
* @throws UnsupportedOperationException if the country is not supported
49+
*/
50+
public String generateId(final Citizen citizen, final Country country) {
51+
final IdGenerator idGenerator = IdGenerator.newInstance(country);
52+
return idGenerator.generate(citizen);
53+
}
4054
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.github.reducktion.socrates.generator;
2+
3+
import com.github.reducktion.socrates.extractor.Citizen;
4+
import com.github.reducktion.socrates.extractor.Gender;
5+
6+
/**
7+
* Generates a new CPR for the provided information
8+
*
9+
* CPR logic:
10+
* * https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)
11+
* * https://da.wikipedia.org/wiki/CPR-nummer
12+
*/
13+
class DenmarkIdGenerator implements IdGenerator {
14+
private static final int[] MULTIPLIERS = { 4, 3, 2, 7, 6, 5, 4, 3, 2, 1 };
15+
16+
@Override
17+
public String generate(final Citizen citizen) {
18+
if (!isRequiredDataPresent(citizen)) {
19+
throw new IllegalArgumentException(
20+
"Date of birth and gender information is necessary to generate a Danish CPR"
21+
);
22+
}
23+
24+
final String dateOfBirth = String.format("%02d", citizen.getDayOfBirth().get())
25+
+ String.format("%02d", citizen.getMonthOfBirth().get())
26+
+ getYearString(citizen.getYearOfBirth().get());
27+
28+
final String centuryDigit = getCenturyDigit(citizen.getYearOfBirth().get());
29+
final String checkDigit = getCheckDigit(citizen.getGender().get());
30+
31+
final int sum = calculateCheckSum(dateOfBirth + centuryDigit + "00" + checkDigit);
32+
final int ceilingValue = (int) Math.ceil(((double) sum) / 11);
33+
final int remainder = (ceilingValue * 11) - sum;
34+
final String generatedDigits = findFinalDigits(remainder);
35+
36+
return dateOfBirth + "-" + centuryDigit + generatedDigits + checkDigit;
37+
}
38+
39+
private static boolean isRequiredDataPresent(final Citizen citizen) {
40+
return citizen.getYearOfBirth().isPresent()
41+
&& citizen.getMonthOfBirth().isPresent()
42+
&& citizen.getDayOfBirth().isPresent()
43+
&& citizen.getGender().isPresent();
44+
}
45+
46+
private static String getYearString(final Integer yearOfBirth) {
47+
final int lastDigits = Integer.parseInt(yearOfBirth.toString().substring(2));
48+
return String.format("%02d", lastDigits);
49+
}
50+
51+
private static String getCenturyDigit(final Integer yearOfBirth) {
52+
if (yearOfBirth < 1999) {
53+
return "3";
54+
}
55+
56+
return yearOfBirth < 2036 ? "4" : "5";
57+
}
58+
59+
private static String getCheckDigit(final Gender gender) {
60+
return Gender.FEMALE == gender ? "2" : "3";
61+
}
62+
63+
private static int calculateCheckSum(final String cpr) {
64+
int sum = 0;
65+
for (int i = 0; i < cpr.length() && i < MULTIPLIERS.length; i++) {
66+
final int digit = Character.getNumericValue(cpr.charAt(i));
67+
sum += digit * MULTIPLIERS[i];
68+
}
69+
return sum;
70+
}
71+
72+
private static String findFinalDigits(final double targetSum) {
73+
if (targetSum / MULTIPLIERS[7] < 1 && targetSum / MULTIPLIERS[8] < 1) {
74+
return findFinalDigits(targetSum + 11);
75+
}
76+
77+
if (targetSum % MULTIPLIERS[7] == 0) {
78+
return (int) targetSum / MULTIPLIERS[7] + "0";
79+
}
80+
81+
if (targetSum % MULTIPLIERS[8] == 0) {
82+
return "0" + (int) targetSum / MULTIPLIERS[8];
83+
}
84+
85+
for (int i = 1; i <= 9; i++) {
86+
for (int j = 1; j <= 9; j++) {
87+
if (targetSum == MULTIPLIERS[7] * i + MULTIPLIERS[8] * j) {
88+
return String.valueOf(i) + j;
89+
}
90+
}
91+
}
92+
93+
throw new ArithmeticException("Could not generate a valid cpr for this data. Please open an issue in https://github.com/reducktion/socrates-java/issues");
94+
}
95+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.reducktion.socrates.generator;
2+
3+
import com.github.reducktion.socrates.Country;
4+
import com.github.reducktion.socrates.extractor.Citizen;
5+
6+
public interface IdGenerator {
7+
8+
/**
9+
* Generates an identifier based on the {@link Citizen} information provided.
10+
*/
11+
String generate(final Citizen citizen);
12+
13+
/**
14+
* Return a new instance of {@link IdGenerator}, that is specific for the country parameter.
15+
*
16+
* @throws UnsupportedOperationException if the country is not supported
17+
*/
18+
static IdGenerator newInstance(final Country country) {
19+
switch (country) {
20+
case DK: return new DenmarkIdGenerator();
21+
default: throw new UnsupportedOperationException("Country not supported.");
22+
}
23+
}
24+
}

src/test/java/com/github/reducktion/socrates/SocratesTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,20 @@ void extractCitizenFromId_shouldReturnCitizen_whenIdForItalyIsValid() {
5151
assertThat(result.isPresent(), is(true));
5252
assertThat(result.get(), is(expectedCitizen));
5353
}
54+
55+
@Test
56+
void generateIdFromCitizen_shouldReturnId_whenDenmarkCitizenIsValid() {
57+
final Citizen citizen = Citizen
58+
.builder()
59+
.gender(Gender.MALE)
60+
.yearOfBirth(1991)
61+
.monthOfBirth(6)
62+
.dayOfBirth(16)
63+
.gender(Gender.MALE)
64+
.build();
65+
66+
final String id = socrates.generateId(citizen, Country.DK);
67+
68+
assertThat(id, is("160691-3113"));
69+
}
5470
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.github.reducktion.socrates.generator;
2+
3+
import static org.hamcrest.CoreMatchers.is;
4+
import static org.hamcrest.MatcherAssert.assertThat;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import java.util.stream.Stream;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
15+
import com.github.reducktion.socrates.extractor.Citizen;
16+
import com.github.reducktion.socrates.extractor.Gender;
17+
18+
class DenmarkIdGeneratorTest {
19+
20+
private DenmarkIdGenerator denmarkIdGenerator;
21+
22+
@BeforeEach
23+
void setup() {
24+
denmarkIdGenerator = new DenmarkIdGenerator();
25+
}
26+
27+
@Test
28+
void validate_exceptionIsThrown_withInvalidCitizen() {
29+
assertThrows(IllegalArgumentException.class, () -> {
30+
denmarkIdGenerator.generate(new Citizen.Builder().build());
31+
});
32+
}
33+
34+
@ParameterizedTest(name = "#{index} - Test with Argument={0},{1},{2}")
35+
@MethodSource("testCitizenProvider")
36+
void validate_cprIsReturned_withValidCitizen(final int year, final int month, final int day,
37+
final Gender gender, final String cpr) {
38+
assertThat(denmarkIdGenerator.generate(
39+
new Citizen.Builder()
40+
.yearOfBirth(year)
41+
.monthOfBirth(month)
42+
.dayOfBirth(day)
43+
.gender(gender)
44+
.build()
45+
), is(cpr));
46+
}
47+
48+
static Stream<Arguments> testCitizenProvider() {
49+
return Stream.of(
50+
Arguments.arguments(1991, 6, 16, Gender.MALE, "160691-3113"),
51+
Arguments.arguments(1984, 10, 8, Gender.FEMALE, "081084-3012")
52+
);
53+
}
54+
}

0 commit comments

Comments
 (0)