diff --git a/src/main/java/com/github/reducktion/socrates/Socrates.java b/src/main/java/com/github/reducktion/socrates/Socrates.java index fe60a81..c18626f 100644 --- a/src/main/java/com/github/reducktion/socrates/Socrates.java +++ b/src/main/java/com/github/reducktion/socrates/Socrates.java @@ -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; /** @@ -37,4 +38,17 @@ public Optional 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); + } } diff --git a/src/main/java/com/github/reducktion/socrates/generator/DenmarkIdGenerator.java b/src/main/java/com/github/reducktion/socrates/generator/DenmarkIdGenerator.java new file mode 100644 index 0000000..e17542b --- /dev/null +++ b/src/main/java/com/github/reducktion/socrates/generator/DenmarkIdGenerator.java @@ -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"); + } +} diff --git a/src/main/java/com/github/reducktion/socrates/generator/IdGenerator.java b/src/main/java/com/github/reducktion/socrates/generator/IdGenerator.java new file mode 100644 index 0000000..8bd7147 --- /dev/null +++ b/src/main/java/com/github/reducktion/socrates/generator/IdGenerator.java @@ -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."); + } + } +} diff --git a/src/test/java/com/github/reducktion/socrates/SocratesTest.java b/src/test/java/com/github/reducktion/socrates/SocratesTest.java index 856fb0c..75eceef 100644 --- a/src/test/java/com/github/reducktion/socrates/SocratesTest.java +++ b/src/test/java/com/github/reducktion/socrates/SocratesTest.java @@ -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")); + } } diff --git a/src/test/java/com/github/reducktion/socrates/generator/DenmarkIdGeneratorTest.java b/src/test/java/com/github/reducktion/socrates/generator/DenmarkIdGeneratorTest.java new file mode 100644 index 0000000..73cd770 --- /dev/null +++ b/src/test/java/com/github/reducktion/socrates/generator/DenmarkIdGeneratorTest.java @@ -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 testCitizenProvider() { + return Stream.of( + Arguments.arguments(1991, 6, 16, Gender.MALE, "160691-3113"), + Arguments.arguments(1984, 10, 8, Gender.FEMALE, "081084-3012") + ); + } +}