Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/main/java/com/github/reducktion/socrates/Socrates.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.github.reducktion.socrates.extractor.Citizen;
import com.github.reducktion.socrates.extractor.CitizenExtractor;
import com.github.reducktion.socrates.generator.IdGenerator;
import com.github.reducktion.socrates.validator.IdValidator;

/**
Expand Down Expand Up @@ -37,4 +38,17 @@ public Optional<Citizen> extractCitizenFromId(final String id, final Country cou
final CitizenExtractor citizenExtractor = CitizenExtractor.newInstance(country);
return citizenExtractor.extractFromId(id, idValidator);
}

/**
* Generates an National Identification Number based on a citizen information.
*
* @param citizen the citizen information
* @param country the country of the national identification number
* @return national identifier string
* @throws UnsupportedOperationException if the country is not supported
*/
public String generateId(final Citizen citizen, final Country country) {
final IdGenerator idGenerator = IdGenerator.newInstance(country);
return idGenerator.generate(citizen);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.github.reducktion.socrates.generator;

import com.github.reducktion.socrates.extractor.Citizen;
import com.github.reducktion.socrates.extractor.Gender;

/**
* Generates a new CPR for the provided information
*
* CPR logic:
* * https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)
* * https://da.wikipedia.org/wiki/CPR-nummer
*/
class DenmarkIdGenerator implements IdGenerator {
private static final int[] MULTIPLIERS = { 4, 3, 2, 7, 6, 5, 4, 3, 2, 1 };

@Override
public String generate(final Citizen citizen) {
if (!isRequiredDataPresent(citizen)) {
throw new IllegalArgumentException(
"Date of birth and gender information is necessary to generate a Danish CPR"
);
}

final String dateOfBirth = String.format("%02d", citizen.getDayOfBirth().get())
+ String.format("%02d", citizen.getMonthOfBirth().get())
+ getYearString(citizen.getYearOfBirth().get());

final String centuryDigit = getCenturyDigit(citizen.getYearOfBirth().get());
final String checkDigit = getCheckDigit(citizen.getGender().get());

final int sum = calculateCheckSum(dateOfBirth + centuryDigit + "00" + checkDigit);
final int ceilingValue = (int) Math.ceil(((double) sum) / 11);
final int remainder = (ceilingValue * 11) - sum;
final String generatedDigits = findFinalDigits(remainder);

return dateOfBirth + "-" + centuryDigit + generatedDigits + checkDigit;
}

private static boolean isRequiredDataPresent(final Citizen citizen) {
return citizen.getYearOfBirth().isPresent()
&& citizen.getMonthOfBirth().isPresent()
&& citizen.getDayOfBirth().isPresent()
&& citizen.getGender().isPresent();
}

private static String getYearString(final Integer yearOfBirth) {
final int lastDigits = Integer.parseInt(yearOfBirth.toString().substring(2));
return String.format("%02d", lastDigits);
}

private static String getCenturyDigit(final Integer yearOfBirth) {
if (yearOfBirth < 1999) {
return "3";
}

return yearOfBirth < 2036 ? "4" : "5";
}

private static String getCheckDigit(final Gender gender) {
return Gender.FEMALE == gender ? "2" : "3";
}

private static int calculateCheckSum(final String cpr) {
int sum = 0;
for (int i = 0; i < cpr.length() && i < MULTIPLIERS.length; i++) {
final int digit = Character.getNumericValue(cpr.charAt(i));
sum += digit * MULTIPLIERS[i];
}
return sum;
}

private static String findFinalDigits(final double targetSum) {
if (targetSum / MULTIPLIERS[7] < 1 && targetSum / MULTIPLIERS[8] < 1) {
return findFinalDigits(targetSum + 11);
}

if (targetSum % MULTIPLIERS[7] == 0) {
return (int) targetSum / MULTIPLIERS[7] + "0";
}

if (targetSum % MULTIPLIERS[8] == 0) {
return "0" + (int) targetSum / MULTIPLIERS[8];
}

for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
if (targetSum == MULTIPLIERS[7] * i + MULTIPLIERS[8] * j) {
return String.valueOf(i) + j;
}
}
}

throw new ArithmeticException("Could not generate a valid cpr for this data. Please open an issue in https://github.com/reducktion/socrates-java/issues");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.github.reducktion.socrates.generator;

import com.github.reducktion.socrates.Country;
import com.github.reducktion.socrates.extractor.Citizen;

public interface IdGenerator {

/**
* Generates an identifier based on the {@link Citizen} information provided.
*/
String generate(final Citizen citizen);

/**
* Return a new instance of {@link IdGenerator}, that is specific for the country parameter.
*
* @throws UnsupportedOperationException if the country is not supported
*/
static IdGenerator newInstance(final Country country) {
switch (country) {
case DK: return new DenmarkIdGenerator();
default: throw new UnsupportedOperationException("Country not supported.");
}
}
}
16 changes: 16 additions & 0 deletions src/test/java/com/github/reducktion/socrates/SocratesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,20 @@ void extractCitizenFromId_shouldReturnCitizen_whenIdForItalyIsValid() {
assertThat(result.isPresent(), is(true));
assertThat(result.get(), is(expectedCitizen));
}

@Test
void generateIdFromCitizen_shouldReturnId_whenDenmarkCitizenIsValid() {
final Citizen citizen = Citizen
.builder()
.gender(Gender.MALE)
.yearOfBirth(1991)
.monthOfBirth(6)
.dayOfBirth(16)
.gender(Gender.MALE)
.build();

final String id = socrates.generateId(citizen, Country.DK);

assertThat(id, is("160691-3113"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.github.reducktion.socrates.generator;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import com.github.reducktion.socrates.extractor.Citizen;
import com.github.reducktion.socrates.extractor.Gender;

class DenmarkIdGeneratorTest {

private DenmarkIdGenerator denmarkIdGenerator;

@BeforeEach
void setup() {
denmarkIdGenerator = new DenmarkIdGenerator();
}

@Test
void validate_exceptionIsThrown_withInvalidCitizen() {
assertThrows(IllegalArgumentException.class, () -> {
denmarkIdGenerator.generate(new Citizen.Builder().build());
});
}

@ParameterizedTest(name = "#{index} - Test with Argument={0},{1},{2}")
@MethodSource("testCitizenProvider")
void validate_cprIsReturned_withValidCitizen(final int year, final int month, final int day,
final Gender gender, final String cpr) {
assertThat(denmarkIdGenerator.generate(
new Citizen.Builder()
.yearOfBirth(year)
.monthOfBirth(month)
.dayOfBirth(day)
.gender(gender)
.build()
), is(cpr));
}

static Stream<Arguments> testCitizenProvider() {
return Stream.of(
Arguments.arguments(1991, 6, 16, Gender.MALE, "160691-3113"),
Arguments.arguments(1984, 10, 8, Gender.FEMALE, "081084-3012")
);
}
}